server-side-props.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import type { GetServerSidePropsContext, GetServerSidePropsResult } from 'next';
  2. import { isIPageNotFoundInfo } from '@growi/core';
  3. import { pagePathUtils } from '@growi/core/dist/utils';
  4. import {
  5. SupportedAction,
  6. type SupportedActionType,
  7. } from '~/interfaces/activity';
  8. import type { CrowiRequest } from '~/interfaces/crowi-request';
  9. import { getServerSideBasicLayoutProps } from '../basic-layout-page';
  10. import {
  11. getServerSideCommonEachProps,
  12. getServerSideCommonInitialProps,
  13. getServerSideI18nProps,
  14. } from '../common-props';
  15. import {
  16. getServerSideGeneralPageProps,
  17. getServerSideRendererConfigProps,
  18. } from '../general-page';
  19. import { isValidGeneralPageInitialProps } from '../general-page/type-guards';
  20. import { addActivity } from '../utils/activity';
  21. import { mergeGetServerSidePropsResults } from '../utils/server-side-props';
  22. import { NEXT_JS_ROUTING_PAGE } from './consts';
  23. import {
  24. getPageDataForInitial,
  25. getPageDataForSameRoute,
  26. } from './page-data-props';
  27. import type { Stage2EachProps, Stage2InitialProps } from './types';
  28. const nextjsRoutingProps = {
  29. props: {
  30. nextjsRoutingPage: NEXT_JS_ROUTING_PAGE,
  31. },
  32. };
  33. /**
  34. * Emit page seen event
  35. * @param context - Next.js server-side context
  36. * @param pageId - Page ID to mark as seen
  37. */
  38. function emitPageSeenEvent(
  39. context: GetServerSidePropsContext,
  40. pageId?: string,
  41. ): void {
  42. if (pageId == null) {
  43. return;
  44. }
  45. const req = context.req as CrowiRequest;
  46. const { user, crowi } = req;
  47. if (user == null) {
  48. return;
  49. }
  50. const pageEvent = crowi.events.page;
  51. pageEvent.emit('seen', pageId, user);
  52. }
  53. function getActivityAction(params: {
  54. isIdenticalPathPage: boolean;
  55. isForbidden?: boolean;
  56. isNotFound?: boolean;
  57. path?: string;
  58. }): SupportedActionType {
  59. if (params.isIdenticalPathPage) {
  60. return SupportedAction.ACTION_PAGE_NOT_CREATABLE;
  61. }
  62. if (params.isForbidden) {
  63. return SupportedAction.ACTION_PAGE_FORBIDDEN;
  64. }
  65. if (params.isNotFound) {
  66. return SupportedAction.ACTION_PAGE_NOT_FOUND;
  67. }
  68. if (params.path != null && pagePathUtils.isUsersHomepage(params.path)) {
  69. return SupportedAction.ACTION_PAGE_USER_HOME_VIEW;
  70. }
  71. return SupportedAction.ACTION_PAGE_VIEW;
  72. }
  73. export async function getServerSidePropsForInitial(
  74. context: GetServerSidePropsContext,
  75. ): Promise<GetServerSidePropsResult<Stage2InitialProps>> {
  76. const [
  77. commonEachResult,
  78. commonInitialResult,
  79. basicLayoutResult,
  80. generalPageResult,
  81. rendererConfigResult,
  82. i18nPropsResult,
  83. pageDataResult,
  84. ] = await Promise.all([
  85. getServerSideCommonEachProps(context),
  86. getServerSideCommonInitialProps(context),
  87. getServerSideBasicLayoutProps(context),
  88. getServerSideGeneralPageProps(context),
  89. getServerSideRendererConfigProps(context),
  90. getServerSideI18nProps(context, ['translation']),
  91. getPageDataForInitial(context),
  92. ]);
  93. // Merge all results in a type-safe manner (using sequential merging)
  94. const mergedResult: GetServerSidePropsResult<Stage2InitialProps> =
  95. mergeGetServerSidePropsResults(
  96. commonEachResult,
  97. mergeGetServerSidePropsResults(
  98. commonInitialResult,
  99. mergeGetServerSidePropsResults(
  100. basicLayoutResult,
  101. mergeGetServerSidePropsResults(
  102. generalPageResult,
  103. mergeGetServerSidePropsResults(
  104. rendererConfigResult,
  105. mergeGetServerSidePropsResults(
  106. i18nPropsResult,
  107. mergeGetServerSidePropsResults(
  108. pageDataResult,
  109. nextjsRoutingProps,
  110. ),
  111. ),
  112. ),
  113. ),
  114. ),
  115. ),
  116. );
  117. // Check for early return (redirect/notFound)
  118. if ('redirect' in mergedResult || 'notFound' in mergedResult) {
  119. return mergedResult;
  120. }
  121. const mergedProps = await mergedResult.props;
  122. // Type-safe props validation AFTER skipSSR is properly set
  123. if (!isValidGeneralPageInitialProps(mergedProps)) {
  124. throw new Error('Invalid merged props structure');
  125. }
  126. // Add user to seen users
  127. emitPageSeenEvent(context, mergedProps.pageWithMeta?.data?._id);
  128. // Persist activity
  129. const activityAction = (() => {
  130. const meta = mergedProps.pageWithMeta?.meta;
  131. if (isIPageNotFoundInfo(meta)) {
  132. return getActivityAction({
  133. isIdenticalPathPage: mergedProps.isIdenticalPathPage,
  134. isForbidden: meta.isForbidden,
  135. isNotFound: meta.isNotFound,
  136. });
  137. }
  138. return getActivityAction({
  139. isIdenticalPathPage: mergedProps.isIdenticalPathPage,
  140. path: mergedProps.pageWithMeta?.data?.path,
  141. });
  142. })();
  143. addActivity(context, activityAction);
  144. return mergedResult;
  145. }
  146. export async function getServerSidePropsForSameRoute(
  147. context: GetServerSidePropsContext,
  148. ): Promise<GetServerSidePropsResult<Stage2EachProps>> {
  149. const [i18nPropsResult, pageDataForSameRouteResult] = await Promise.all([
  150. getServerSideI18nProps(context, ['translation']),
  151. getPageDataForSameRoute(context),
  152. ]);
  153. const { props: pageDataProps, internalProps } = pageDataForSameRouteResult;
  154. // Add user to seen users
  155. emitPageSeenEvent(
  156. context,
  157. internalProps?.pageWithMeta?.data?._id?.toString(),
  158. );
  159. // Persist activity
  160. const activityAction = (() => {
  161. const meta = internalProps?.pageWithMeta?.meta;
  162. if (isIPageNotFoundInfo(meta)) {
  163. return getActivityAction({
  164. isIdenticalPathPage: pageDataProps.isIdenticalPathPage,
  165. isForbidden: meta.isForbidden,
  166. isNotFound: meta.isNotFound,
  167. });
  168. }
  169. return getActivityAction({
  170. isIdenticalPathPage: pageDataProps.isIdenticalPathPage,
  171. path: internalProps?.pageWithMeta?.data?.path,
  172. });
  173. })();
  174. addActivity(context, activityAction);
  175. const mergedResult: GetServerSidePropsResult<Stage2EachProps> =
  176. mergeGetServerSidePropsResults({ props: pageDataProps }, i18nPropsResult);
  177. return mergedResult;
  178. }